跳到主要内容

动态 TSX 渲染和数据绑定

  基础 TSX 教程介绍了如何用 TSX 描述 Dora 节点,并通过 toNode() 创建节点。本篇补充说明 DoraX 中更接近 React 的动态渲染路径:createRoot()signal()、hooks、keyed diff,以及哪些情况下 DoraX 会 patch 或重建引擎节点。

一次性创建和动态根节点

  toNode(element) 是一次性转换。DoraX 会遍历 TSX 树,创建 Dora 节点,设置属性,然后返回创建出的节点或节点组。它适合静态场景片段、预制对象,以及创建后主要由引擎 API 或你的代码直接控制的对象。

  createRoot(parent) 会在一个已有 Dora 节点下创建动态 TSX 根节点。动态 root 会记住上一次渲染出的元素树。更新时,DoraX 会重新执行渲染函数,把新的树和旧树比较,并尽量以较小的代价更新引擎场景树。

import { Director } from 'Dora';
import { React, createRoot, signal } from 'DoraX';

const count = signal(0);
const root = createRoot(Director.entry);

root.render(() => (
<label fontName="sarasa-mono-sc-regular" fontSize={48}>
Clicks: {count.value}
</label>
));

Director.entry.onTapped(() => {
count.value += 1;
});

  当 count.value 变化时,DoraX 会调度读取过该 signal 的 root,在 Dora 的调度器上更新它们。上面的 label 节点会被复用,只更新文本内容。

  当动态 UI 或场景片段不再需要时,调用 root.unmount()。它会移除渲染出的节点,清理 ref,调用 onUnmount,取消 signal 订阅,并避免后续 signal 变化继续更新这个 root。

Signals 和 Hooks

  DoraX 区分模块级状态工具和组件内 hooks。

API使用位置用途
signal(value)模块作用域或普通代码可驱动动态 root 更新的共享响应式状态。
reference(value?)模块作用域或普通代码在 hooks 外创建节点或值引用。
useSignal(value)只能在函数组件内组件本地响应式状态,跨 render 保持同一实例。
useRef(value?)建议只在函数组件内组件本地可变引用,跨 render 保持同一对象。在组件外调用时会输出 warning,并为兼容旧代码退回 reference(value?)
useMemo(factory, deps)只能在函数组件内缓存计算结果,直到依赖变化。
useCallback(callback, deps)只能在函数组件内保持回调函数身份稳定,直到依赖变化。

  Hooks 应在 DoraX 函数组件内调用。在组件外调用 useSignaluseMemouseCallback 会报错。useRef 为兼容旧代码,在组件外调用时只会输出 warning,并返回 reference(value?)。新的组件外代码仍建议直接使用 signal()reference()

import { React, useCallback, useSignal } from 'DoraX';

function CounterButton() {
const count = useSignal(0);
const onTapped = useCallback(() => {
count.value += 1;
}, [count.value]);

return (
<label
fontName="sarasa-mono-sc-regular"
fontSize={36}
onTapped={onTapped}
>
{count.value}
</label>
);
}

  对 useMemo()useCallback(),DoraX 会在编译期检查依赖数组。如果回调闭包里使用了某个值但没有写进依赖数组,编译器会报缺失依赖。如果依赖数组里写了回调没有使用的值,编译器会报告多余依赖。这个检查很重要,因为回调身份变化可能导致某些节点重建,例如 <custom-node>

动态列表中的稳定 key

  当同级列表会插入、删除、过滤或重排时,应给每一项提供稳定的 key。DoraX 使用 key 找回应该被新元素复用的旧节点。

interface MenuItem {
id: number;
text: string;
}

const items = signal<MenuItem[]>([
{ id: 1, text: "Start" },
{ id: 2, text: "Options" },
{ id: 3, text: "Exit" },
]);

root.render(() => (
<node>
{items.value.map(item => (
<label key={item.id} fontName="sarasa-mono-sc-regular" fontSize={30}>
{item.text}
</label>
))}
</node>
));

  如果列表从 [1, 2, 3, 4] 变为 [4, 2, 1, 3],带 key 的节点会保持身份不变。DoraX 还会更新复用节点的 order,让实际的 parent.children 顺序跟随新的 TSX 顺序,除非元素显式设置了自己的 order

  没有 key 的子节点会按下标匹配。固定布局中可以这样做,但动态列表不应省略 key。

Patch、重建和带清理的 Patch

  动态更新时,DoraX 会在四种行为中选择一种:

行为含义示例
Patch复用 Dora 节点,并更新变化的属性。位置、缩放、文本、多数普通节点属性。
子树重建unmount 旧节点及其子节点,再 mount 新子树。元素类型变化、key 变化。
宿主重建只替换当前 Dora 节点。已有子节点会移动到新节点下,再继续和新的 children 做 diff。资源 file 变化、<custom-node onCreate> 变化、只在初始化阶段生效的属性变化。
带清理的 patch复用节点,但清理或替换引擎侧注册。替换事件回调、替换 ref、修改 onUpdate

  多数普通属性会直接赋值 patch。当一个普通属性从新的 TSX 元素中移除时,DoraX 通常不会给引擎属性写入 undefined,而是保留之前的引擎值。只有属性本身支持 undefined,或存在明确清理 API 时,DoraX 才会做专门清理。

常见重建场景

元素或属性重建条件
所有元素元素类型变化或 key 变化会触发子树重建。
onMount修改、添加或删除 onMount 都会重建,因为它只在 mount 阶段执行。
<draw-node>任意更新都会重建,因为 draw shape 子节点是即时绘制命令。
资源节点spriteplayablespinemodelaudio-sourceparticletile-nodevideo-node 等节点的 file 变化会重建。
<label>fontNamefontSizesdf 变化会重建。文本内容本身会 patch。
<body>结构性的 body 设置和 fixture 子节点变化会重建 body。
<custom-node>onCreate 变化会重建 custom node。
<align-node>windowRoot 变化会重建 align node。

  除了元素类型和 key 变化,重建通常表示宿主重建:DoraX 会替换当前引擎节点,把仍然匹配的已挂载子节点移动到新节点下,再继续执行普通 keyed children diff。子节点自身也需要重建时会重建,被删除的子节点会 unmount,新增子节点会 mount。

特殊 patch 场景

元素或属性Patch 行为
ref写入 ref.current = node;替换、删除或 unmount 时清理旧 ref。
onTappedonKeyDownonContactStart 等 slot 事件清理旧 slot 回调并注册新回调。删除属性时清理 slot。
输入事件新增 tap、keyboard 或 controller 事件时,会自动开启对应的引擎开关,除非属性显式禁用。
onUpdate注册新的函数或 job;删除时调用 unschedule()
onRender替换渲染阶段回调时先调用 clearRender(),再注册新回调。删除属性时调用 clearRender()
onContactFilter替换当前 filter 回调。
<physics-world> 下的 <contact>调用 setShouldContact() 更新碰撞规则,不重建 physics world。
<playable play>playloop 变化时重新调用 play()
<audio-source playMode>playModedelayTime 变化时调用对应播放方法。
<particle emit>变化为 true 时 start(),变化为 false 时 stop()
<align-node style>重新生成 CSS 文本并调用 css()
<line verts>顶点或颜色变化时调用 line.set()

事件回调身份

  多数 Dora slot 风格事件属性是可 patch 的,因此回调变化不会重建节点。DoraX 会清理旧 slot,再注册新函数。

  custom-node.onCreate 不同。它是创建 Dora 节点本身的函数,所以函数身份变化时,DoraX 必须重建节点。

  在函数组件中返回 <custom-node> 时,应使用 useCallback() 保持 onCreate 稳定:

import { React, useCallback } from 'DoraX';
import * as ButtonCreate from 'UI/Control/Basic/Button';

function Button(props: { key?: number; text: string; onClick: () => void }) {
const createButton = useCallback(() => {
const button = ButtonCreate({
text: props.text,
width: 80,
height: 48,
});
button.onTapped(props.onClick);
return button;
}, [props.text, props.onClick]);

return <custom-node key={props.key} onCreate={createButton} />;
}

  如果漏写 props.text,编译器会报告缺失依赖。如果依赖数组里写了回调没有使用的值,编译器会报告多余依赖。

Action 子节点

  <move-x><sequence><loop> 等 action 元素是命令型子节点。当 action 子树变化时,DoraX 会重新构造 action,并在宿主节点上再次执行。删除 action 子节点不会主动停止已经运行的 action。

  默认 action 子节点使用 runAction(),因此多个 action 可以并行运行。如果新 action 应该通过 perform() 替换节点当前动作,可以添加 exclusive

<node>
<move-x time={0.2} start={0} stop={120} />
<scale exclusive time={0.2} start={1} stop={1.2} />
</node>

  如果同一轮渲染中出现多个 exclusive action,DoraX 会把兼容的 action 用 Spawn(...) 合并。如果同一宿主节点上同时出现 <loop exclusive> 和非 loop 的 exclusive action,DoraX 会按源码顺序选择先出现的独占组,忽略冲突组,并输出 warning。

实用规则

  • 只需要一次性创建时使用 toNode()
  • TSX 需要跟随数据变化时使用 createRoot()
  • 组件外使用 signal()reference()
  • 函数组件内使用 useSignal()useRef()useMemo()useCallback()
  • 动态同级列表必须提供稳定 key。
  • useCallback() 保持 <custom-node onCreate> 稳定。
  • file、body fixture、draw shape 等结构性输入看作重建边界。
  • 节点被 diff 删除时需要清理逻辑,就使用 onUnmount